﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using CoreRay.Samplers;
using CoreRay.Cameras;
using CoreRay.Integrators;
using CoreRay.Filters;
using CoreRay.RenderRegionManagers;

namespace CoreRay
{
    public class RendererAsyncResult : IAsyncResult
    {
        public RendererAsyncResult(object state)
        {
            this.AsyncState = state;
        }

        public object AsyncState
        {
            get;
            private set;
        }

        public WaitHandle AsyncWaitHandle
        {
            get;
            private set;
        }

        public bool CompletedSynchronously
        {
            get;
            private set;
        }

        public bool IsCompleted
        {
            get;
            private set;
        }
    }

    class ASyncParameters
    {
        public ASyncParameters(Scene scene, AsyncCallback callback)
        {
            this.Scene = scene;
            this.Callback = callback;
        }

        public Scene Scene
        {
            get;
            private set;
        }

        public AsyncCallback Callback
        {
            get;
            private set;
        }
    }

    public class RenderRegionUpdatedEventArgs : EventArgs
    {
        public RenderRegionUpdatedEventArgs(RenderRegion renderRegion)
        {
            this.RenderRegion = renderRegion;
        }

        public RenderRegion RenderRegion
        {
            get;
            private set;
        }
    }

    public class Renderer : IDisposable
    {        
        private SurfaceIntegrator surfaceIntegrator;
        private Thread[] renderThreads;
        private Thread mainRenderThread;
        private RendererAsyncResult currentAsyncResult;
        private RenderRegionManager regionManager;

        public event EventHandler<RenderRegionUpdatedEventArgs> RenderRegionUpdating;
        public event EventHandler<RenderRegionUpdatedEventArgs> RenderRegionUpdated;

        public Renderer() : this(new RenderSettings())
        {
            
        }

        public Renderer(RenderSettings renderSettings)
        {
            this.RenderAccelerators = new List<RenderAccelerator>();
            CoreRayContext.Current.AddRenderer(this, renderSettings);

            Filter filter = new BoxFilter(0.5f, 0.5f);
            this.Film = new Film(CoreRayContext.Current.RenderSettings.RenderWidth, CoreRayContext.Current.RenderSettings.RenderHeight, filter);
            this.regionManager = new BasicRenderRegionManager(this.Film.Width, this.Film.Height);
            this.renderThreads = new Thread[Environment.ProcessorCount];

            ParameterizedThreadStart threadStart = new ParameterizedThreadStart(ProcessRenderWorker);

            for(int i = 0; i < this.renderThreads.Length; i++)
            {
                this.renderThreads[i] = new Thread(threadStart);
                this.renderThreads[i].Name = "RenderThread" + i;
            }
        }

        public void Dispose()
        {

        }

        public Film Film
        {
            get;
            private set;
        }

        public List<RenderAccelerator> RenderAccelerators
        {
            get;
            private set;
        }

        public void AddRenderAccelerator(RenderAccelerator renderingAccelerator)
        {
            this.RenderAccelerators.Add(renderingAccelerator);
        }

        #region Async Pattern
        public IAsyncResult BeginRenderScene(Scene scene, AsyncCallback callback, object state)
        {
            ParameterizedThreadStart threadStart = new ParameterizedThreadStart(RenderScene);

            this.mainRenderThread = new Thread(threadStart);
            this.mainRenderThread.Name = "MainRenderThread";
            this.mainRenderThread.Start(new ASyncParameters(scene, callback));

            this.currentAsyncResult = new RendererAsyncResult(state);
            return this.currentAsyncResult;
        }

        public void EndRenderScene(IAsyncResult asyncResult)
        {
            this.mainRenderThread.Join();
        }

        private void RenderScene(object parameters)
        {
            ASyncParameters asyncParameters = parameters as ASyncParameters;

            RenderScene(asyncParameters.Scene);

            asyncParameters.Callback(this.currentAsyncResult);
        }
        #endregion

        public void RenderScene(Scene scene)
        {
            CoreRayContext.Current.LogManager.Write("Renderer", "Prepare for rendering...");

            scene.MainCamera.Init(this.Film);

            //renderingAccelerator = null;
            if (this.RenderAccelerators.Count > 0)
            {
                try
                {
                    this.RenderAccelerators[0].InitScene(scene);
                }

                catch
                {
                    this.RenderAccelerators.Clear();
                    CoreRayContext.Current.LogManager.Write(LogInformationTypes.Warning, "Renderer", "Failed to intialize rendering accelerator!");
                }
            }

            this.surfaceIntegrator = new WhittedIntegrator(scene);

            //Call the integrators Pre-Render method
            this.surfaceIntegrator.OnPreRender();

            DateTime datetime = DateTime.Now;

            //Launch render threads
            for(int i = 0; i < this.renderThreads.Length; i++)
            {
                this.renderThreads[i].Start(scene);
            }

            //Wait for the render threads to finish their jobs
            for(int i = 0; i < this.renderThreads.Length; i++)
            {
                this.renderThreads[i].Join();
            }

            CoreRayContext.Current.LogManager.Write("Renderer", "Render done in {0}.", (DateTime.Now - datetime).ToString());

            //Write the film to an image
            this.Film.WriteImage("output.bmp");
        }

        private void ProcessRenderWorker(object parameter)
        {
            CoreRayContext.Current.LogManager.Write("Renderer", "Starting {0}...", Thread.CurrentThread.Name);

            Scene scene = (Scene) parameter;

            RenderRegion renderRegion = GetNextRenderRegion();

            while(renderRegion != null)
            {
                if (this.RenderRegionUpdating != null)
                {
                    this.RenderRegionUpdating(this, new RenderRegionUpdatedEventArgs(renderRegion));
                }

                ProcessRenderRegion(scene, renderRegion);

                this.Film.UpdatePreviewImage(renderRegion);

                if (this.RenderRegionUpdated != null)
                {
                    this.RenderRegionUpdated(this, new RenderRegionUpdatedEventArgs(renderRegion));
                }

                renderRegion = GetNextRenderRegion();
            }
        }

        private RenderRegion GetNextRenderRegion()
        {
            RenderRegion renderRegion = null;

            lock(this.regionManager)
            {
                renderRegion = this.regionManager.GetNextRenderRegion();
            }

            return renderRegion;
        }

        private void ProcessRenderRegion(Scene scene, RenderRegion renderRegion)
        {
            var intersections = (this.RenderAccelerators.Count > 0) ? this.RenderAccelerators[0].FindPrimaryIntersections(scene, renderRegion) : this.FindPrimaryIntersections(scene, renderRegion);

            //Main loop
            foreach (var intersection in intersections)
            {
                Spectrum ls = Li(intersection.Ray, intersection);
                //Spectrum ls = new Spectrum(1);
                
                //Add sample color to the film
                this.Film.AddSample(intersection.Sample, ls);
            }
        }

        private IEnumerable<Intersection> FindPrimaryIntersections(Scene scene, RenderRegion renderRegion)
        {
            Sampler sampler = new StratifiedSampler(renderRegion, 1, 1, false);

            //Main loop
            foreach (Sample sample in sampler.GenerateSamples())
            {
                //Generate a new ray from the camera for the current sample
                Ray ray = scene.MainCamera.GenerateRay(sample);

                Intersection intersection = scene.Intersect(ray);

                if (intersection != null)
                {
                    intersection.Sample = sample;
                    intersection.Ray = ray;

                    yield return intersection;
                }
            }
        }

        private Spectrum Li(Ray ray, Intersection primaryIntersection)
        {
            Spectrum lo = this.surfaceIntegrator.LI(ray, primaryIntersection);

            //Just return lo for the moment
            return lo;
        }
    }
}
